AWS CloudFront 와 ReverseProxy + Caching

CloudFront와 네트워크를 같이 이해해보려고 한다.
HTTP Cache-Control에 대한 포스트 를 함께 보면 좋을 것 같다.

간단하게 CloudFront에 대해 알아보자면

AWS 네트워크는 물리적으로 Region, AZ 그리고 논리적으로 VPC, Subnet으로 나뉘어있다.
하지만 다른 서비스와 달리, Cloudfront는 저 4개에 속해 있지 않다.

CloudFront는 AWS의 CDN(Content Delivery Network) 서비스로,
VPC 밖, 전 세계에 흩어져 있는 엣지 로케이션(Edge Location)에 위치하여
캐싱된 컨텐츠를 DNS상 사용자에게 가장 가까운 곳에서 빠르게 응답해준다.

  • 서울, LA, 프랑크푸르트… 이런 데 퍼져 있는 Edge Location에서 요청을 받고
  • 가능한 건 Edge에서 바로 응답을 대신 준다.
  • 필요하면 Origin(S3, ALB 등) 으로 요청을 보내고 받아온 후 응답한다.

CloudFront 서비스는 “리버스 프록시 + 캐시 레이어” 라고 보면 된다

Origin이 뭔데?

CloudFront가 전 세계에 퍼진 리버스 프록시 라면, Origin은 그 프록시 뒤에 있는 실제 서비스 다.

CloudFront에 붙일 수 있는 Origin의 종류는 다양하다.

  • S3 : 정적 파일, 이미지, JS/CSS
  • ALB : Next.js, Node 같은 앱 서버
  • API Gateway / Lambda : Serverless API

실제로 CloudFront에 붙일 수 있는 Origin에는 위와 같은 것들이 있다.
하나의 CloudFront Distribution 는 단일 Origin 뿐만 아니라, Multi Origin도 가능하다.

그래서

/static/*  → S3 Origin
/_next/*   → S3 Origin
/images/*  → 이미지 리사이징 Origin
/api/*     → API Gateway Origin
/*         → ALB Origin (SSR)

이런 식으로 하나의 CloudFront 배포에 여러 Origin을 붙이고,
경로(Path Pattern)로 라우팅할 수 있다.


Reverse Proxy로써의 CloudFront

CloudFront의 캐싱을 빼고 Reverse Proxy의 역할을 생각해보자. 프록시는 중간에서 요청을 대신 받아서 Origin으로 보내고, 응답을 다시 클라이언트에게 전달해주는 중개자다. 매 요청마다 Origin으로 다시 요청을 보내서 그 응답을 클라이언트에 내려준다. 응답을 어디에도 저장하지 않고 단순하게 “경유” 만 시킨다.

CloudFront에서 Origin 마다 Cache Policy를 설정하게 되는데,
Cache PolicyCachingDisabled로 지정하면: 리버스 프록시 용도로만 CloudFront를 쓰게된다.
CloudFront는 응답을 Edge에 저장하지 않고(캐싱하지 않고) 같은 요청이 들어와도 매번 CloudFront를 경유하여 Origin으로 간다

이런 설정은 보통 ALB Origin (SSR, API) 에 쓴다.
캐싱을 잘못 켜면 사용자별·쿠키별로 달라야 할 HTML이 섞여 나와서 큰 사고가 날 수 있기 때문이다.

다만, 이 과정에서 Reverse Proxy로써의 CloudFront에서는 헤더 추가/삭제, URL 리라이트, 라우팅, 인증, WAF, 로깅, DDoS 방어 이런 걸 할 수 있어서, 쉽게 maintenance ui를 보여준다던가, 특정 IP를 차단한다던가를 할 수 있다.

Cache Behavior

Cache Behavior 는 CloudFront가 특정 경로에 대해 어떤 Origin과 어떤 정책을 사용할지 정하는 규칙 세트로
Behavior 안에서 아래를 정의한다.

  • 어떤 Path Pattern을 매칭할지?
  • 어떤 Origin으로 보낼지?
  • Cache Policy는 무엇인지?
  • Origin Request Policy는 무엇인지?
  • 어떤 헤더/쿠키를 Origin으로 넘길지?
  • 어떤 Lambda@Edge / CloudFront Function을 붙일지?

Cache Behavior 설정을 통해 어떤 요청이 오면 어떤 오리진을 통해 리소스를 가져올 것인지 결정한다.
이 Cache Behavior에서 어떤 헤더들을 Origin으로 넘길지 등 정책을 결정할 수도 있다.
또, 여기서 내부 정책인 Cache Policy 설정을 통해 캐싱도 제어 할 수 있다.


CloudFront의 Caching

CloudFront에서 캐싱은 두 가지로 제어된다.

  • CloudFront가 가진 자체 캐싱 규칙(Cache Policy)
  • Origin(S3, ALB, EC2 등)이 내려주는 Cache-Control 헤더

CloudFront는 Origin이 응답하는 모든것을 그대로 내려주는 것이 기본 동작이지만,
실제로는 CloudFront의 TTL 규칙이 더 우선된다.
그래서 Origin이 no-store를 내려도 CloudFront가 캐싱하는 상황이 발생한다.

CloudFront TTL과 Origin의 Cache-Control

CloudFront에서 Caching Policy를 설정하고
Orgin으로 바라보고 있는 S3에 Cache Policy를 설정하거나,
EC2 같은 인스턴스에서 Cache-Control 헤더를 설정한다.

각각 어떻게 동작하게 될까? 충돌이 발생하면 어떻게 될까?
Origin이 이렇게 Cache-Control 헤더를 내려줘도

Cache-Control: private, no-cache, no-store, s-maxage=0

CloudFront TTL이 60초로 설정되어 있다면,
privateno-store가 있더라도 CloudFront는 이 리소스를 60초 동안 캐싱한다.
Origin의 no-store는 CloudFront 기준에선 “브라우저에게 주는 지시”일 뿐이라서
S3의 Cache-Control, ALB의 no-store보다 CloudFront의 TTL 설정이 우선이다.

마찬가지로 s-maxage가 길게 잡혀있어도 TTL이 우선된다.

Cache-Control : public, s-maxage=396000

CloudFront Cache-Key

캐싱 동작에서 TTL만큼 중요한 것이 Cache Key다.
CloudFront는 어떤 요청을 “동일 요청”이라고 판단할지를 Cache Key로 정한다.
Cache Key에 포함할 수 있는 요소는 다음과 같다.

- URL 경로
- QueryString
- Cookie
- Header(User-Agent 등)

이 중 무엇을 포함하느냐에 따라 캐시가 다르게 저장된다.
이렇게 서로 다른 조건으로 캐시가 여러 버전 생기는 것을 Variant라고 한다.
CloudFront는 기본적으로 URL 경로만을 기준으로 캐시키를 잡기 때문에,
쿠키나 쿼리스트링을 사용하여 데이터가 변하는 경우 Variant를 설정해줘야한다.

이렇게 Variant를 설정하면, 응답에 Vary 헤더에 설정한 캐시키 종류가 내려오게 될 것이고
캐시가 적중하면 x-cache : Hit from cloudfront 라는 응답 헤더가 보이게 된다.
만약 캐시가 적중하지 않았다면, x-cache : Miss from cloudfront라는 응답헤더가 보인다.

Variant가 많아지면 캐시가 잘게 쪼개지고 CloudFront는 Hit Ratio를 잃는다.
그래서 특히 SSR 페이지를 캐싱할 때는 캐시 조각화쿠키 등 Variant 설정을 잘 해야 한다.


Lambda@edge와 Cloudfront Function

CloudFront는 단순히 응답을 캐싱하는 CDN이 아니라. 엣지에서 코드를 실행할 수 있는 플랫폼이기도 하다.

CloudFront Function

  • 아주 가벼움
  • V8 기반
  • Viewer Request/Response 단계에서만 동작
  • 리다이렉트, 간단한 헤더 조작, URL rewrite 같은 작은 작업에 적합

Lambda@Edge

  • Node.js 전체 실행 가능
  • Viewer/Origin 모든 단계에서 동작
  • 이미지 리사이징, 인증, 쿠키 검증처럼 무거운 작업 처리 가능

보통 이미지 변환, Geo 기반 라우팅, 사용자 인증 등은 Lambda@Edge가 담당하고,
단순한 헤더 조작이나 redirect는 CloudFront Function으로 처리한다.

그냥 Lambda와 Lambda@edge의 차이는 뭘까?

둘 다 “Lambda”지만, 실행되는 위치와 쓰는 목적이 완전히 다르다. Lambda는 특정 AWS Region 안에서 실행되는 서버리스 함수다. 백엔드 API를 만들거나, 배치 작업을 돌리거나, DB·SQS 같은 AWS 내부 리소스를 다룰 때 사용한다. 즉, “내 서버 로직을 서버 없이 Region에서 실행하고 싶을 때” 쓰는 함수다.

반면 Lambda@Edge는 CloudFront의 Edge Location에서 실행되는 Lambda다. 사용자와 가장 가까운 위치에서 요청·응답을 가로채서 URL을 바꾸거나, 쿠키를 심거나, 헤더를 변경하거나, 인증·리다이렉트·이미지 변환 같은 작업을 처리한다. 즉, “Origin까지 가지 말고 요청을 그 자리(Edge)에서 처리하고 싶을 때” 쓰는 함수다.


Next.js를 쓸 때, CloudFront 사용 예시

Next.js를 배포하면 결과물이 크게 두 묶음으로 나뉠텐데
정적 파일(.next/static, 이미지, 폰트 등)은 S3에 올리고,
서버 실행 파일(RSC/SSR 코드)은 ECS나 EC2에 올려서 ALB 뒤에 두게 될 것이다.

CloudFront는 이 둘을 Multi Origin 형태로 묶어
한 배포(Distribution)에서 경로별로 라우팅하게 만들 수 있다.

- /_next/static/* → S3 Origin
- /static/* → S3 Origin
- 그 외 모든 페이지(/) → ALB Origin

S3 Origin 설정

AWS가 S3 Origin을 붙일 때 추천하는 CachingOptimized 설정을 그대로 사용하면 된다. 이 정책은 긴 TTL과 immutable 캐싱을 기본으로 한다.

왜냐면 Next.js가 빌드될 때 정적 파일의 이름에 해시가 붙기 때문이다.
파일 내용이 바뀌면 해시가 달라지고 파일명 자체가 바뀌므로,
stale한 데이터를 볼 일이 없다.

ALB Origin 설정

ALB 뒤에는 SSR이 있고, HTML은 사용자 상태에 따라 달라질 수 있다.

쿠키 내용이 다르면 다른 UI가 나올 수 있고
A/B 테스트 중이면 HTML 자체가 변할 수 있고
User-Agent마다 다른 SSR을 반환할 수도 있고

이런 경우 CloudFront가 SSR HTML을 캐싱하면 다른 사용자의 HTML이 섞여 나오는 사고가 발생한다. 그래서 ALB Origin은 보통 CachingDisabled를 쓴다. CloudFront는 프록시 역할만 하고 캐시하지 않는다는 뜻이다.

만약 최적화를 위해 SSR HTML을 캐싱한다면, 필요한 일부 값만 Cache Key로 넣어 Variant를 관리해야 한다. 특정 쿠키, User-Agent, Authorization 헤더 등이런 요소를 Cache Key에 포함시키면 CloudFront는 조건별로 다른 캐시를 관리할 수 있다. 하지만 Variant가 많아지면 Hit Ratio가 떨어지므로 매우 신중해야 한다.

이미지 리사이징 등 최적화 처리

Next 서버에서 이미지를 바로 내려줄 수 있지만, 보통 이건 네트워크 트래픽을 발생해 성능에 문제가 생길 수 있다.
그래서 개인적으론 S3에서 이미지 서빙을 선호하는데,

정적 이미지 파일을 그대로 S3에서 서빙하는 건 최적화 관점에서 좋지 않을 때가 많다.
브라우저에서 불필요하게 큰 사이즈, 큰 퀄리티의 이미지를 내려받아서 느리게 이미지가 등장하거나
많은 네트워크 비용이 발생할 수 있다.

그럴 때, Lambda@Edge를 사용해 이미지 리사이징이 되도록 만들 수 있다.

1. /my-image?q=60&width=300의 요청에서 쿼리스트링의 퀄리티, 사이즈 정보를 읽어서
2. S3에서 원본 이미지 가져와서
3. 리사이즈/웹포맷 변환 등을 수행한 뒤
4. CloudFront 캐시에 저장하고 내려준다.

이런 구조를 만들면 전 세계 Edge에서 최적화된 이미지를 바로 서빙할 수 있다.